package me.huanmeng.heater.event;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import lombok.Getter;
import me.huanmeng.heater.bot.MessageHelper;
import me.huanmeng.heater.exception.EventException;
import me.huanmeng.heater.plugin.Plugin;
import me.huanmeng.heater.plugin.PluginClassLoader;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;

/**
 * 2021/10/3<br>
 * Heater<br>
 * 事件处理<br>
 * @author huanmeng_qwq
 */
public final class EventBus implements MessageHelper {

    private final Multimap<Class<? extends Event>, MethodData> registeredMethods = HashMultimap.create();
    private final Multimap<Plugin, MethodData> registeredPluginEventMethods = HashMultimap.create();

    public EventBus() {
    }

    public void registerEvent(Plugin plugin, Object object) {
        final Class<?> clazz = object.getClass();
        final List<MethodData> methodDatas = getMethodDatas(plugin, object, object.getClass(), clazz.getAnnotation(EventHandler.class));
        putMethods(methodDatas);
    }

    public void unregisterEvent(Object object) {
        final Class<?> clazz = object.getClass();
        final List<MethodData> methodDatas = getMethodDatas(object, clazz.getAnnotation(EventHandler.class) == null);
        for (MethodData methodData : methodDatas) {
            registeredMethods.remove(methodData.getEventClass(), methodData);
            registeredPluginEventMethods.remove(methodData.getPlugin(), methodData);
        }
    }

    public void unregisterEvents(@NotNull Plugin plugin) {
        if (!registeredPluginEventMethods.containsKey(plugin)) {
            return;
        }
        final Collection<MethodData> methodDatas = registeredPluginEventMethods.get(plugin);
        for (MethodData methodData : methodDatas) {
            registeredMethods.remove(methodData.getEventClass(), methodData);
        }
        registeredPluginEventMethods.removeAll(plugin);
    }

    final void putMethods(@NotNull List<MethodData> methodData) {
        for (MethodData data : methodData) {
            registeredMethods.put(data.getEventClass(), data);
            if (data.getPlugin() != null) {
                registeredPluginEventMethods.put(data.getPlugin(), data);
            }
        }
    }

    public <T extends Event> T callEvent(T event) {
        for (MethodData data : getMethodDatas(event.getClass())) {
            try {
                data.execute(event);
            } catch (Exception e) {
                log(Level.SEVERE, String.format("Plugin %s execute event %s failed.", data.getPluginName(), data.getMethod().getName()), e);
            }
        }
        return event;
    }

    final List<MethodData> getMethodDatas(Class<? extends Event> eventClass) {
        if (!registeredMethods.containsKey(eventClass)) {
            return Collections.emptyList();
        }
        List<MethodData> list = new ArrayList<>(registeredMethods.get(eventClass));
        list.sort(Comparator.comparingInt(MethodData::getPriority));
        return Collections.unmodifiableList(list);
    }

    final List<MethodData> getMethodDatas(Object instance, boolean check) {
        List<Class<?>> methodClasses = new ArrayList<>();
        final Class<?> clazz = instance.getClass();
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.getParameterCount() != 1) {
                continue;
            }
            if (!Event.class.isAssignableFrom(method.getParameterTypes()[1])) {
                continue;
            }
            if (method.getAnnotation(EventHandler.class) != null || !check) {
                methodClasses.add(method.getParameterTypes()[0]);
            }
        }
        List<MethodData> methodData = new ArrayList<>();
        for (Class<?> c : methodClasses) {
            if (!registeredMethods.containsKey(c)) {
                continue;
            }
            //noinspection unchecked
            methodData.addAll(
                    registeredMethods.get((Class<? extends Event>) c)
                            .stream()
                            .filter(e -> clazz.isAssignableFrom(e.getClazz()))
                            .collect(Collectors.toList())
            );
        }
        return methodData;
    }

    final List<MethodData> getMethodDatas(@Nullable Plugin plugin, Object instance, Class<?> clazz, EventHandler eventHandler) {
        List<MethodData> list = new ArrayList<>();
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.getParameterCount() != 1) {
                continue;
            }
            if (!Event.class.isAssignableFrom(method.getParameterTypes()[0])) {
                continue;
            }
            if (method.getAnnotation(EventHandler.class) != null || eventHandler != null) {
                list.add(new MethodData(plugin, clazz, instance, method, eventHandler));
            }
        }
        return list;
    }

    @SuppressWarnings("FieldCanBeLocal")
    private static class MethodData implements MessageHelper {
        @Getter
        private final Plugin plugin;
        private final Object instance;
        @Getter
        private final Class<?> clazz;
        @Getter
        private final Class<? extends Event> eventClass;
        @Getter
        private final Method method;
        private EventHandler eventHandler;
        @Getter
        private final int priority;
        private Tip tip;

        public MethodData(@Nullable Plugin plugin, Class<?> clazz, Object instance, Method method, EventHandler eventHandler) {
            if (!Event.class.isAssignableFrom(method.getParameterTypes()[0])) {
                throw new EventException(String.format("[%s] Class %s method %s parameter is not event class", getPluginName(), clazz.getName(), method.getName()));
            }
            if (clazz.getAnnotation(Tip.class) == null && plugin == null) {
                throw new EventException("If plugin == null,You must add @Tip annotation in class");
            }
            this.tip = clazz.getAnnotation(Tip.class);
            this.plugin = plugin;
            this.clazz = clazz;
            this.method = method;
            this.eventHandler = eventHandler;
            if (this.eventHandler == null) {
                final EventHandler annotation = method.getAnnotation(EventHandler.class);
                if (annotation == null) {
                    throw new EventException(String.format("[%s] Annotation EventHandler not found.", getPluginName()));
                }
                this.eventHandler = annotation;
            }
            if (tip != null && clazz.getClassLoader() instanceof PluginClassLoader) {
                log(Level.WARNING, String.format("Class %s register as a listener, Reason: %s, It may be registered by %s"
                        , clazz.getCanonicalName()
                        , tip.value()
                        , ((PluginClassLoader) clazz.getClassLoader()).getPlugin().getDescription().getFullName()
                        )
                );
            }
            this.priority = this.eventHandler.priority().ordinal();
            //noinspection unchecked
            this.eventClass = (Class<? extends Event>) this.method.getParameterTypes()[0];
            this.instance = instance;
            this.method.setAccessible(true);
        }

        private String getPluginName() {
            return plugin == null ? tip == null ? "Heater" : tip.value() : plugin.getDescription().getName();
        }

        <T extends Event> void execute(T event) throws InvocationTargetException, IllegalAccessException {
            this.method.invoke(instance, event);
        }

        @Override
        public String toString() {
            return "MethodData{" +
                    "plugin=" + plugin +
                    ", instance=" + instance +
                    ", clazz=" + clazz +
                    ", eventClass=" + eventClass +
                    ", method=" + method +
                    ", eventHandler=" + eventHandler +
                    ", priority=" + priority +
                    '}';
        }
    }
}
